package com.xuecheng.content.service.impl;

import com.alibaba.fastjson.JSON;
import com.xuecheng.base.constant.CoursePublishStatusConstant;
import com.xuecheng.content.constant.CourseCacheConstant;
import com.xuecheng.content.model.dto.CoursePreviewDTO;
import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.content.service.ICourseOpenService;
import com.xuecheng.content.service.ICoursePublishService;
import com.xuecheng.content.service.bloom.CourseBloomFilterInitializer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author 勾新杰
 * @version 1.0
 * @description: 课程公开查询接口实现类
 * @date 2025/2/14 20:38
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CourseOpenServiceImpl implements ICourseOpenService {

    /**
     * redis模板
     */
    private final RedisTemplate<String, String> redisTemplate;

    /**
     * redisson客户端
     */
    private final RedissonClient redissonClient;

    /**
     * 课程发布服务，这里不能用课程预览接口，因为那个接口查询到的有可能是未发布的
     */
    private final ICoursePublishService coursePublishService;

    /**
     * 布隆过滤器初始化器
     */
    private final CourseBloomFilterInitializer courseBloomFilterInitializer;

    /**
     * 根据课程id查询课程预览信息
     *
     * @param courseId 课程id
     * @return 课程预览信息
     */
    @Override
    public CoursePreviewDTO getCoursePreviewInfo(Long courseId) {
        // 0. 先查询布隆过滤器，如果布隆过滤器中不存在，直接返回空，预防缓存穿透
        RBloomFilter<Long> courseBloomFilter = courseBloomFilterInitializer.getCourseBloomFilter();
        if (!courseBloomFilter.contains(courseId))
            return CoursePreviewDTO.builder().build();
        // 1. 先查询redis缓存
        String jsonstring = redisTemplate.opsForValue().get(CourseCacheConstant.KEY_PREFIX + courseId);
        // 2. 如果存在，直接返回，存的时候直接用CoursePreviewDTO，取的时候不用转换
        if (jsonstring != null) {
            return JSON.parseObject(jsonstring, CoursePreviewDTO.class);
        }
        // 3. 如果不存在，查询数据库，查到之后存到redis中，下次直接从redis中取
        else {
            // synchronized (this) { // 对于缓存击穿，可以设置锁，来保证只有一个线程查询数据库，其他线程等待，
            // 这样加锁还有个问题：不适用于分布式，因为对于多台节点服务器而言，各自都有自己的锁，那么锁就不唯一了。
            // 但是这样性能不好，所以第二种：就是先获取一把分布式锁，再创建一个新的线程去查询数据库并完成redis的同步，
            // 当前线程就返回null，其他线程发现缓存过期，也需要获取分布式锁，如果获取失败，说明有人在干事，直接返回null
            // 新的线程完成数据库到redis的同步后需要释放分布式锁
            // 分布式锁的获取方式1：SET_NX
            // String lockValue = UUID.randomUUID().toString();
            // 3.1 获取分布式锁，注释部分是通过set nx实现的分布式锁
            // Boolean set = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + courseId, lockValue, LOCK_EXPIRE_MINUTES, TimeUnit.MINUTES);
            RLock lock = redissonClient.getLock(CourseCacheConstant.LOCK_PREFIX + courseId);
            // 3.2 如果获取到分布式锁
            // if (Boolean.TRUE.equals(set)) {
            try {
                // 自旋锁，在指定等待时间内一直自旋准备获取锁
                boolean isLock = lock.tryLock(CourseCacheConstant.LOCK_TRY_WAIT_SECONDS, TimeUnit.SECONDS);
                if (isLock) {
                    // new Thread(() -> {
                    try {
                        // 3.1 查询数据库，条件：课程id，状态为已发布
                        CoursePublish coursePublish = coursePublishService.lambdaQuery()
                                .eq(CoursePublish::getId, courseId)
                                .eq(CoursePublish::getStatus, CoursePublishStatusConstant.PUBLISHED)
                                .oneOpt()
                                .orElse(null);
                        // 3.2 如果不存在，返回空的
                        if (coursePublish == null) {
                            // 如果未找到，设置特殊值存入redis并设置1分钟后过期，避免缓存穿透
                            redisTemplate.opsForValue().set(CourseCacheConstant.KEY_PREFIX + courseId, CourseCacheConstant.ESPECIAL_VALUE, CourseCacheConstant.ESPECIAL_VALUE_EXPIRE_SECONDS, TimeUnit.MINUTES);
                            return CoursePreviewDTO.builder().build();
                        }
                        // 3.3 如果存在，存到redis，返回课程预览信息
                        else {
                            // 3.3.1 封装课程基本信息、课程营销信息、课程计划、课程教师
                            CoursePreviewDTO coursePreviewDTO = coursePublishService.getCoursePublishWhole(courseId);
                            // 3.3.2 存到redis中
                            redisTemplate.opsForValue().set(CourseCacheConstant.KEY_PREFIX + courseId, JSON.toJSONString(coursePreviewDTO), CourseCacheConstant.KEY_EXPIRE_MINUTES, TimeUnit.MINUTES);
                            // 3.3.3 往布隆过滤器中增加一项
                            courseBloomFilter.add(courseId);
                            return coursePreviewDTO;
                        }
                    } catch (Exception e) {
                        log.error("课程预览信息缓存出错：{}", e.getMessage());
                        return CoursePreviewDTO.builder().build();
                    } finally {
                        // 3.4 释放分布式锁，释放的时候要检查是否是父线程获取的锁，不是则不删除，因为有可能子线程还没做完锁就过期了，此时其它线程获得锁
                        // if (Objects.equals(redisTemplate.opsForValue().get(LOCK_PREFIX + courseId), lockValue)) {
                        //     // 这里还有个问题：线程不安全，如果刚判断完另外一个线程上处理器，锁也刚好过期，那么此线程再上处理机就会把人家的锁删了
                        //     // 这里只要实现原子性就行了，但这需要lua脚本实现
                        //     redisTemplate.delete(LOCK_PREFIX + courseId);
                        // }
                        // 释放锁，释放的时候redisson会自己去看是不是当前线程持有的，也会发送lua脚本保证原子性
                        lock.unlock();
                    }
                    // }).start();
                }
                // 没获取到分布式锁，直接返回空
                else {
                    return CoursePreviewDTO.builder().build();
                }
            } catch (InterruptedException e) {
                log.error("获取分布式锁出错：{}", e.getMessage());
                return CoursePreviewDTO.builder().build();
            }
            // // }
            // // 不管获取锁到锁还是没获取到锁，都返回空，大大提高性能
            // return CoursePreviewDTO.builder().build();
            // // }
        }
    }
}
